'''
dqn 玩平衡车的视频
'''
import random
import gym
import numpy as np
from collections import deque
import tensorflow as tf
import os

EPISODES = 5000

# DQNAgent 代理
class DQNAgent:
    def __init__(self, state_size, action_size):
        '''
        :param state_size: 状态的类型数量
        :param action_size:  动作的类型数量
        '''
        self.state_size = state_size
        self.action_size = action_size
        self.memory = deque(maxlen=2000) # 训练的数据最长的内存长度
        self.gamma = 0.95 # gamma 期望计算的遗忘比例
        self.epsilon = 1.
        self.epsilon_min = 0.01
        self.epsilon_decay = 0.995
        self.learning_rate = 0.001
        self.model = self._build_model()

    def _build_model(self):
        '''
        构建dqn训练模型
        训练的模型网络为全连接网络
        :return:
        '''
        model = tf.keras.models.Sequential()
        model.add(tf.keras.layers.Dense(24, input_dim=self.state_size, activation='relu'))
        model.add(tf.keras.layers.Dense(24, activation='relu'))
        model.add(tf.keras.layers.Dense(self.action_size, activation='linear'))
        model.compile(loss='mse', optimizer=tf.keras.optimizers.Adam(lr=self.learning_rate))

        return model

    def remember(self, state, action, reward, next_state, done):
        '''
        记录每次随机尝试后所带来的奖励等各种信息
        :param state: 当前的状态
        :param action: 执行的动作
        :param reward: 执行动作后带来的奖励
        :param next_state: 下一个状态
        :param done: 是否结束
        :return:
        '''
        self.memory.append((state, action, reward, next_state, done))

    def act(self, state):
        '''
        根据状态判断下一步要执行的动作
        :param state:
        :return: 返回模型预测的下一个状态
        '''
        if np.random.rand() <= self.epsilon:
            return random.randrange(self.action_size)

        act_values = self.model.predict(state)
        return np.argmax(act_values[0])

    def replay(self, batch_size):
        minibatch = random.sample(self.memory, batch_size)
        for state, action, reward, next_state, done in minibatch:
            target = reward
            '''
            以下逻辑的原理为：
            如果游戏没有done，表示所作的动作是正确的，那么继续增加动作的值以保证
            激励网络此方向的正确性。
            如果游戏已经done，表示所作的动作是错误的，那么将反馈值赋值到错误的动作上
            拉开与正确动作的差值
            也就是利用这种方式实现自监督强化学习
            所以强化学习的原理在于不断的尝试，将能够使游戏结束的操作的值不断的减小，
            使游戏结束的操作减小后，正确的操作就能够被选中，这就是基础强化学习的原理
            '''
            if not done:
                target = (reward + self.gamma * np.amax(self.model.predict(next_state)[0]))

            target_f = self.model.predict(state)
            target_f[0][action] = target

            self.model.fit(state, target_f, epochs=1, verbose=0)

        if self.epsilon > self.epsilon_min:
            self.epsilon *= self.epsilon_decay

    def load(self, name):
        if os.path.exists(name):
            self.model.load_weights(name)

    def save(self, name):
        self.model.save_weights(name)

if __name__ == "__main__":
    env = gym.make('CartPole-v0')
    state_size = env.observation_space.shape[0]
    action_size = env.action_space.n
    agent = DQNAgent(state_size, action_size)
    agent.load('./weights/cartpole-dqn.h5')
    done = False
    batch_size = 32

    for e in range(EPISODES):
        state = env.reset()
        state = np.reshape(state, [1, state_size])

        for time in range(500):
            env.render()
            action = agent.act(state)
            next_state, reward, done, _ = env.step(action)
            reward = reward if not done else -10
            next_state = np.reshape(next_state, [1, state_size])
            agent.remember(state, action, reward, next_state, done)
            state = next_state

            if done:
                print("episode: {}/{}, score: {}, e: {:.2}".format(e, EPISODES, time, agent.epsilon))
                break


        if len(agent.memory) > batch_size:
            agent.replay(batch_size)
            if e % 10 == 0:
                agent.save("./weights/cartpole-dqn.h5")